ํ์ด์ฌ์ ์ฌ์ฉํ์ฌ ๊ฐ๋ ฅํ OLAP ์์คํ ๊ณผ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค๋ฅผ ์ค๊ณํ๊ณ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. ์ด ๊ฐ์ด๋๋ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง ๋ฐ ETL๋ถํฐ Pandas, Dask, DuckDB์ ๊ฐ์ ์ ํฉํ ๋๊ตฌ ์ ํ๊น์ง ๋ชจ๋ ๊ฒ์ ๋ค๋ฃน๋๋ค.
ํ์ด์ฌ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ง: OLAP ์์คํ ์ค๊ณ ์ข ํฉ ๊ฐ์ด๋
์ค๋๋ ๋ฐ์ดํฐ ์ค์ฌ์ ์ธ๊ณ์์ ๋ฐฉ๋ํ ์์ ์ ๋ณด๋ฅผ ์ ์ํ๊ฒ ๋ถ์ํ๋ ๋ฅ๋ ฅ์ ๋จ์ํ ๊ฒฝ์ ์ฐ์๊ฐ ์๋๋ผ ํ์์ ๋๋ค. ์ ์ธ๊ณ ๋น์ฆ๋์ค๋ ์์ฅ ๋ํฅ์ ์ดํดํ๊ณ , ์ด์์ ์ต์ ํํ๋ฉฐ, ์ ๋ต์ ์์ฌ๊ฒฐ์ ์ ๋ด๋ฆฌ๊ธฐ ์ํด ๊ฐ๋ ฅํ ๋ถ์์ ์์กดํฉ๋๋ค. ์ด๋ฌํ ๋ถ์ ๋ฅ๋ ฅ์ ํต์ฌ์๋ ๋ ๊ฐ์ง ๊ธฐ๋ณธ ๊ฐ๋ ์ด ์์ต๋๋ค. ๋ฐ๋ก ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค(DWH)์ ์จ๋ผ์ธ ๋ถ์ ์ฒ๋ฆฌ(OLAP) ์์คํ ์ ๋๋ค.
์ ํต์ ์ผ๋ก ์ด๋ฌํ ์์คํ ์ ๊ตฌ์ถํ๋ ค๋ฉด ์ ๋ฌธ์ ์ด๊ณ ์ข ์ข ๋ ์ ์ ์ด๋ฉฐ ๊ฐ๋น์ผ ์ํํธ์จ์ด๊ฐ ํ์ํ์ต๋๋ค. ๊ทธ๋ฌ๋ ์คํ์์ค ๊ธฐ์ ์ ๋ถ์์ ๋ฐ์ดํฐ ์์ง๋์ด๋ง์ ๋ฏผ์ฃผํํ์ต๋๋ค. ์ด ์ ๋์ ํ์ด์ฌ์ด ์์ต๋๋ค. ํ์ด์ฌ์ ์๋ํฌ์๋ ๋ฐ์ดํฐ ์๋ฃจ์ ์ ๊ตฌ์ถํ๋ ๋ฐ ํ์ํ ์ ํ์ด ๋ ์ ์๋ ํ๋ถํ ์ํ๊ณ๋ฅผ ๊ฐ์ถ ๋ค์ฌ๋ค๋ฅํ๊ณ ๊ฐ๋ ฅํ ์ธ์ด์ ๋๋ค. ์ด ๊ฐ์ด๋์์๋ ์ ์ธ๊ณ ๋ฐ์ดํฐ ์์ง๋์ด, ์ํคํ ํธ ๋ฐ ๊ฐ๋ฐ์๋ฅผ ๋์์ผ๋ก ํ์ด์ฌ ์คํ์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ง ๋ฐ OLAP ์์คํ ์ ์ค๊ณํ๊ณ ๊ตฌํํ๋ ํฌ๊ด์ ์ธ ์๋ด๋ฅผ ์ ๊ณตํฉ๋๋ค.
ํํธ 1: ๋น์ฆ๋์ค ์ธํ ๋ฆฌ์ ์ค์ ์ด์ - DWH ๋ฐ OLAP
ํ์ด์ฌ ์ฝ๋๋ฅผ ์์ธํ ์ดํด๋ณด๊ธฐ ์ ์ ์ํคํ ์ฒ ์๋ฆฌ๋ฅผ ์ดํดํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์ผ๋ฐ์ ์ธ ์ค์๋ ์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ง์ ๋ถ์์ ์๋ํ๋ ๊ฒ์ธ๋ฐ, ์ด๋ ์ฑ๋ฅ ์ ํ์ ๋ถ์ ํํ ์ธ์ฌ์ดํธ๋ก ์ด์ด์ง ์ ์์ต๋๋ค. ์ด๊ฒ์ด ๋ฐ๋ก ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค์ OLAP๊ฐ ํด๊ฒฐํ๊ธฐ ์ํด ์ค๊ณ๋ ๋ฌธ์ ์ ๋๋ค.
๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค(DWH)๋ ๋ฌด์์ธ๊ฐ?
๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค๋ ํ๋ ์ด์์ ๋ถ์ฐ๋ ์์ค์์ ํตํฉ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ์ค์ ์ง์ค์ ๋ฆฌํฌ์งํ ๋ฆฌ์ ๋๋ค. ์ฃผ์ ๋ชฉ์ ์ ๋น์ฆ๋์ค ์ธํ ๋ฆฌ์ ์ค(BI) ํ๋, ํนํ ๋ถ์ ๋ฐ ๋ณด๊ณ ๋ฅผ ์ง์ํ๋ ๊ฒ์ ๋๋ค. ์กฐ์ง์ ๊ณผ๊ฑฐ ๋ฐ์ดํฐ์ ๋ํ ๋จ์ผ ์ง์ค ๊ณต๊ธ์์ผ๋ก ์๊ฐํ์ญ์์ค.
์ด๋ ์ผ์์ ์ธ ์ ํ๋ฆฌ์ผ์ด์ (์: ์ ์ ์๊ฑฐ๋ ๊ฒฐ์ ์์คํ ๋๋ ์ํ์ ๊ฑฐ๋ ์์ฅ)์ ์ง์ํ๋ ์จ๋ผ์ธ ํธ๋์ญ์ ์ฒ๋ฆฌ(OLTP) ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ๊ทน๋ช ํ๊ฒ ๋์กฐ๋ฉ๋๋ค. ๋ค์์ ๋น ๋ฅธ ๋น๊ต์ ๋๋ค.
- ์ํฌ๋ก๋: OLTP ์์คํ ์ ๋๋์ ์๊ณ ๋น ๋ฅธ ํธ๋์ญ์ (์ฝ๊ธฐ, ์ฝ์ , ์ ๋ฐ์ดํธ)์ ์ฒ๋ฆฌํฉ๋๋ค. DWH๋ ์๋ฐฑ๋ง ๊ฐ์ ๋ ์ฝ๋๋ฅผ ์ค์บํ๋ ์ ์ ์์ ๋ณต์กํ๊ณ ์ฅ๊ธฐ ์คํ ์ฟผ๋ฆฌ(์ฝ๊ธฐ ์ค์ฌ)์ ์ต์ ํ๋์ด ์์ต๋๋ค.
- ๋ฐ์ดํฐ ๊ตฌ์กฐ: OLTP ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ๊ณ ์ค๋ณต์ ํผํ๊ธฐ ์ํด ๊ณ ๋๋ก ์ ๊ทํ๋ฉ๋๋ค. DWH๋ ๋ถ์ ์ฟผ๋ฆฌ๋ฅผ ๋จ์ํํ๊ณ ๊ฐ์ํํ๊ธฐ ์ํด ์ข ์ข ๋น์ ๊ทํ๋ฉ๋๋ค.
- ๋ชฉ์ : OLTP๋ ๋น์ฆ๋์ค๋ฅผ ์ด์ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. DWH๋ ๋น์ฆ๋์ค๋ฅผ ๋ถ์ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
์ ์ค๊ณ๋ DWH๋ ์ ๊ตฌ์ ๋น ์ธ๋ชฌ์๊ฒ ์์ฃผ ๊ธฐ์ธํ๋ ๋ค ๊ฐ์ง ์ฃผ์ ์์ฑ์ผ๋ก ํน์ง์ง์ด์ง๋๋ค.
- ์ฃผ์ ์งํฅ: ๋ฐ์ดํฐ๋ ์ ํ๋ฆฌ์ผ์ด์ ํ๋ก์ธ์ค๊ฐ ์๋ '๊ณ ๊ฐ', '์ ํ' ๋๋ 'ํ๋งค'์ ๊ฐ์ ๋น์ฆ๋์ค์ ์ฃผ์ ์ฃผ์ ๋ฅผ ์ค์ฌ์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
- ํตํฉ: ๋ฐ์ดํฐ๋ ๋ค์ํ ์์ค์์ ์์ง๋์ด ์ผ๊ด๋ ํ์์ผ๋ก ํตํฉ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด 'USA', 'United States', 'U.S.'๋ ๋ชจ๋ ๋จ์ผ 'United States' ํญ๋ชฉ์ผ๋ก ํ์คํ๋ ์ ์์ต๋๋ค.
- ์๊ฐ ๊ฐ๋ณ: ์จ์ดํ์ฐ์ค์ ๋ฐ์ดํฐ๋ ์ฅ๊ธฐ๊ฐ(์: 5-10๋ )์ ์ ๋ณด๋ฅผ ๋ํ๋ด๋ฏ๋ก ๊ณผ๊ฑฐ ๋ถ์ ๋ฐ ์ถ์ธ ์๋ณ์ด ๊ฐ๋ฅํฉ๋๋ค.
- ๋นํ๋ฐ์ฑ: ๋ฐ์ดํฐ๊ฐ ์จ์ดํ์ฐ์ค์ ๋ก๋๋๋ฉด ๊ฑฐ์ ๋๋ ์ ๋ ์ ๋ฐ์ดํธ๋๊ฑฐ๋ ์ญ์ ๋์ง ์์ต๋๋ค. ์ด๋ ๊ณผ๊ฑฐ ์ด๋ฒคํธ์ ์๊ตฌ ๊ธฐ๋ก์ด ๋ฉ๋๋ค.
OLAP(์จ๋ผ์ธ ๋ถ์ ์ฒ๋ฆฌ)๋ ๋ฌด์์ธ๊ฐ?
DWH๊ฐ ๊ณผ๊ฑฐ ๋ฐ์ดํฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๋ฉด OLAP๋ ์ด๋ฅผ ํ์ํ ์ ์๊ฒ ํด์ฃผ๋ ๊ฐ๋ ฅํ ๊ฒ์ ์์ง์ด์ ๋ถ์ ๋๊ตฌ์ ๋๋ค. OLAP๋ ์ฌ์ฉ์๊ฐ ์์ฝ๋ ์ ๋ณด๋ฅผ ๋ค์ฐจ์ ๋ทฐ, ์ฆ OLAP ํ๋ธ๋ก ์ ์ํ๊ฒ ๋ถ์ํ ์ ์๋๋ก ํ๋ ์ํํธ์จ์ด ๊ธฐ์ ๋ฒ์ฃผ์ ๋๋ค.
OLAP ํ๋ธ๋ OLAP์ ๊ฐ๋ ์ ํต์ฌ์ ๋๋ค. ๋ฐ๋์ ๋ฌผ๋ฆฌ์ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ ์๋์ง๋ง ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ธ๋งํ๊ณ ์๊ฐํํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. ํ๋ธ๋ ๋ค์์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
- ์ธก์ ๊ฐ: '์์ต', 'ํ๋งค ์๋' ๋๋ '์ด์ต'๊ณผ ๊ฐ์ด ๋ถ์ํ๋ ค๋ ์ ๋์ ์ซ์ ๋ฐ์ดํฐ ํฌ์ธํธ์ ๋๋ค.
- ์ฐจ์: ์ธก์ ๊ฐ์ ์ค๋ช ํ๊ณ ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํ๋ ๋ฒ์ฃผํ ์์ฑ์ ๋๋ค. ์ผ๋ฐ์ ์ธ ์ฐจ์์๋ '์๊ฐ'(๋ , ๋ถ๊ธฐ, ์), '์ง๋ฆฌ'(๊ตญ๊ฐ, ์ง์ญ, ๋์) ๋ฐ '์ ํ'(์นดํ ๊ณ ๋ฆฌ, ๋ธ๋๋, SKU)์ด ํฌํจ๋ฉ๋๋ค.
ํ๋งค ๋ฐ์ดํฐ ํ๋ธ๋ฅผ ์์ํด ๋ณด์ญ์์ค. ๋ค์ํ ์ฐจ์์์ ์ด ์์ต(์ธก์ ๊ฐ)์ ๋ณผ ์ ์์ต๋๋ค. OLAP๋ฅผ ์ฌ์ฉํ๋ฉด ์ด ํ๋ธ์์ ์์ฒญ๋ ์๋๋ก ๊ฐ๋ ฅํ ์์ ์ ์ํํ ์ ์์ต๋๋ค.
- ์ฌ๋ผ์ด์ค: ํ ์ฐจ์์ ๋ํ ๋จ์ผ ๊ฐ์ ์ ํํ์ฌ ํ๋ธ์ ์ฐจ์์ ์ค์ ๋๋ค. ์: '2023๋ 4๋ถ๊ธฐ'์ ํ๋งค ๋ฐ์ดํฐ๋ง ๋ณด๊ธฐ.
- ๋ฆฌ๋ฒ์ค: ์ฌ๋ฌ ์ฐจ์์ ๋ํ ๊ฐ ๋ฒ์๋ฅผ ์ง์ ํ์ฌ ํ์ ํ๋ธ๋ฅผ ์ ํํฉ๋๋ค. ์: '์ ๋ฝ' ๋ฐ '์์์'(์ง๋ฆฌ ์ฐจ์)์ '์ ์์ ํ' ๋ฐ '์๋ฅ'(์ ํ ์ฐจ์)์ ๋ํ ํ๋งค ๋ณด๊ธฐ.
- ๋๋ฆด ๋ค์ด / ๋๋ฆด ์ : ์ฐจ์ ๋ด์ ์์ธ ์์ค์ ํ์ํฉ๋๋ค. ๋๋ฆด ๋ค์ด์ ์์ฝ ์์ค์ด ๋์ ๊ฒ์์ ์์ธ ์์ค์ด ๋ฎ์ ๊ฒ์ผ๋ก ์ด๋ํฉ๋๋ค(์: '๋ '์์ '๋ถ๊ธฐ'๋ก, '์'๋ก). ๋๋ฆด ์ (๋๋ ๋กค์ )์ ๊ทธ ๋ฐ๋์ ๋๋ค.
- ํผ๋ฒ: ํ๋ธ์ ์ถ์ ํ์ ํ์ฌ ๋ฐ์ดํฐ์ ๋ํ ์๋ก์ด ๋ณด๊ธฐ๋ฅผ ์ป์ต๋๋ค. ์: '์ ํ' ๋ฐ '์ง๋ฆฌ' ์ถ์ ๊ตํํ์ฌ ์ด๋ ์ง์ญ์์ ์ด๋ค ์ ํ์ ๊ตฌ๋งคํ๋์ง ํ์ธํฉ๋๋ค(์ด๋ ์ง์ญ์์ ์ด๋ค ์ ํ์ด ํ๋งค๋๋์ง๊ฐ ์๋๋ผ).
OLAP ์์คํ ์ ํ
OLAP ์์คํ ์๋ ์ธ ๊ฐ์ง ์ฃผ์ ์ํคํ ์ฒ ๋ชจ๋ธ์ด ์์ต๋๋ค.
- MOLAP(๋ค์ฐจ์ OLAP): ์ด๊ฒ์ด 'ํด๋์' ํ๋ธ ๋ชจ๋ธ์ ๋๋ค. ๋ฐ์ดํฐ๋ DWH์์ ์ถ์ถ๋์ด ๋ ์ ๋ค์ฐจ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฌ์ ์ง๊ณ๋ฉ๋๋ค. ์ฅ์ : ๋ชจ๋ ๋ต๋ณ์ด ๋ฏธ๋ฆฌ ๊ณ์ฐ๋์ด ์์ผ๋ฏ๋ก ์ฟผ๋ฆฌ ์ฑ๋ฅ์ด ๋งค์ฐ ๋น ๋ฆ ๋๋ค. ๋จ์ : ์ฌ์ ์ง๊ณ๋ ์ ์๊ฐ ์์ฒญ๋ ์ ์์ผ๋ฏ๋ก '๋ฐ์ดํฐ ํญ๋ฐ'๋ก ์ด์ด์ง ์ ์์ผ๋ฉฐ, ์์์น ๋ชปํ ์ง๋ฌธ์ ๋ตํด์ผ ํ๋ ๊ฒฝ์ฐ ์ ์ฐ์ฑ์ด ๋จ์ด์ง ์ ์์ต๋๋ค.
- ROLAP(๊ด๊ณํ OLAP): ์ด ๋ชจ๋ธ์ ๋ฐ์ดํฐ๋ฅผ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค(์ผ๋ฐ์ ์ผ๋ก DWH ์์ฒด)์ ์ ์งํ๊ณ ์ ๊ตํ ๋ฉํ๋ฐ์ดํฐ ๊ณ์ธต์ ์ฌ์ฉํ์ฌ OLAP ์ฟผ๋ฆฌ๋ฅผ ํ์ค SQL๋ก ๋ณํํฉ๋๋ค. ์ฅ์ : ์ต์ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฑ๋ฅ์ ํ์ฉํ์ฌ ํ์ฅ์ฑ์ด ๋ฐ์ด๋๊ณ ๋ ์์ธํ๊ณ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค. ๋จ์ : ์ง๊ณ๊ฐ ์ฆ์์์ ์ํ๋๋ฏ๋ก ์ฟผ๋ฆฌ ์ฑ๋ฅ์ด MOLAP๋ณด๋ค ๋๋ฆด ์ ์์ต๋๋ค.
- HOLAP(ํ์ด๋ธ๋ฆฌ๋ OLAP): ์ด ์ ๊ทผ ๋ฐฉ์์ ๋ ๊ฐ์ง ์ฅ์ ์ ๋ชจ๋ ๊ฒฐํฉํ๋ ค๊ณ ์๋ํฉ๋๋ค. MOLAP ์คํ์ผ ํ๋ธ์ ๊ณ ์์ค ์ง๊ณ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ฌ ์๋๋ฅผ ๋์ด๊ณ ROLAP ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ธ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ฌ ๋๋ฆด๋ค์ด ๋ถ์์ ์ํํฉ๋๋ค.
ํ์ด์ฌ์ผ๋ก ๊ตฌ์ถ๋ ์ต์ ๋ฐ์ดํฐ ์คํ์ ๊ฒฝ์ฐ ๊ฒฝ๊ณ๊ฐ ๋ชจํธํด์ก์ต๋๋ค. ๋๋๋๋ก ๋น ๋ฅธ ์ดํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฑ์ฅ์ผ๋ก ROLAP ๋ชจ๋ธ์ด ์ง๋ฐฐ์ ์ด ๋๊ณ ๋งค์ฐ ํจ๊ณผ์ ์ด ๋์์ต๋๋ค. ์ข ์ข ์ ํต์ ์ธ MOLAP ์์คํ ์ ํ์ ํ๋ ์ฑ๋ฅ์ ์ ๊ณตํ๋ฉด์๋ ๊ฒฝ์ง์ฑ์ ์ค์ด๋ญ๋๋ค.
ํํธ 2: ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ง์ ์ํ ํ์ด์ฌ ์ํ๊ณ
์ ํต์ ์ผ๋ก ์ํฐํ๋ผ์ด์ฆ BI ํ๋ซํผ์ด ์ง๋ฐฐํ๋ ์์ ์ ์ ํ์ด์ฌ์ ์ ํํด์ผ ํ ๊น์? ๋ต์ ์ ์ฐ์ฑ, ๊ฐ๋ ฅํ ์ํ๊ณ, ๋ฐ์ดํฐ ์๋ช ์ฃผ๊ธฐ ์ ์ฒด๋ฅผ ํตํฉํ๋ ๋ฅ๋ ฅ์ ์์ต๋๋ค.
์ ํ์ด์ฌ์ธ๊ฐ?
- ํตํฉ ์ธ์ด: ํ์ด์ฌ์ ๋ฐ์ดํฐ ์ถ์ถ(ETL), ๋ณํ, ๋ก๋ฉ, ์ค์ผ์คํธ๋ ์ด์ , ๋ถ์, ๋จธ์ ๋ฌ๋ ๋ฐ API ๊ฐ๋ฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ณต์ก์ฑ์ด ์ค์ด๋ค๊ณ ๋ค๋ฅธ ์ธ์ด ๋ฐ ๋๊ตฌ ๊ฐ์ ์ ํํ ํ์์ฑ์ด ์ค์ด๋ญ๋๋ค.
- ๋ฐฉ๋ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ํ๊ณ: ํ์ด์ฌ์ ๋ฐ์ดํฐ ์กฐ์(Pandas, Dask)๋ถํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํธ ์์ฉ(SQLAlchemy) ๋ฐ ์ํฌํ๋ก์ฐ ๊ด๋ฆฌ(Airflow, Prefect)์ ์ด๋ฅด๊ธฐ๊น์ง ๋ชจ๋ ๋จ๊ณ์ ๋ํ ์ฑ์ํ๊ณ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ณด์ ํ๊ณ ์์ต๋๋ค.
- ๊ณต๊ธ์ ์ฒด ๋ฌด๊ด: ํ์ด์ฌ์ ์คํ์์ค์ด๋ฉฐ ๋ชจ๋ ๊ฒ๊ณผ ์ฐ๊ฒฐ๋ฉ๋๋ค. ๋ฐ์ดํฐ๊ฐ PostgreSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค, Snowflake ์จ์ดํ์ฐ์ค, S3 ๋ฐ์ดํฐ ๋ ์ดํฌ ๋๋ Google Sheet์ ์๋ ์ ๊ทผํ ์ ์๋ ํ์ด์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ต๋๋ค.
- ํ์ฅ์ฑ: ํ์ด์ฌ ์๋ฃจ์ ์ ๋ ธํธ๋ถ์์ ์คํ๋๋ ๊ฐ๋จํ ์คํฌ๋ฆฝํธ๋ถํฐ Dask ๋๋ Spark(PySpark ์ฌ์ฉ)์ ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ฐ๋ ํด๋ฌ์คํฐ์์ ํํ๋ฐ์ดํธ์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ถ์ฐ ์์คํ ๊น์ง ํ์ฅํ ์ ์์ต๋๋ค.
๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค ์คํ์ ์ํ ํต์ฌ ํ์ด์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์ผ๋ฐ์ ์ธ ํ์ด์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ง ์๋ฃจ์ ์ ๋จ์ผ ์ ํ์ด ์๋๋ผ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์ ๋ ๋ชจ์์ ๋๋ค. ํ์ ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
ETL/ELT(์ถ์ถ, ๋ณํ, ๋ก๋)
- Pandas: ํ์ด์ฌ์์ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ ์กฐ์์ ์ํ ์ฌ์ค์์ ํ์ค์ ๋๋ค. ๋ช ๊ธฐ๊ฐ๋ฐ์ดํธ๊น์ง์ ์๊ฑฐ๋ ์ค๊ฐ ๊ท๋ชจ ๋ฐ์ดํฐ์ ์ ์ฒ๋ฆฌํ๋ ๋ฐ ์ ํฉํฉ๋๋ค. DataFrame ๊ฐ์ฒด๋ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฆฌ, ๋ณํ ๋ฐ ๋ถ์ํ๋ ๋ฐ ์ง๊ด์ ์ด๊ณ ๊ฐ๋ ฅํฉ๋๋ค.
- Dask: ํ์ด์ฌ ๋ถ์์ ํ์ฅํ๋ ๋ณ๋ ฌ ์ปดํจํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. Dask๋ Pandas API๋ฅผ ๋ชจ๋ฐฉํ์ง๋ง ๋ฐ์ดํฐ๋ฅผ ์ฒญํฌ๋ก ๋ถํ ํ๊ณ ์ฌ๋ฌ ์ฝ์ด ๋๋ ๋จธ์ ์์ ๋ณ๋ ฌ๋ก ์ฒ๋ฆฌํ์ฌ ๋ฉ๋ชจ๋ฆฌ๋ณด๋ค ํฐ ๋ฐ์ดํฐ์ ์ ์ฒ๋ฆฌํ ์ ์๋ ๋ณ๋ ฌ DataFrame ๊ฐ์ฒด๋ฅผ ์ ๊ณตํฉ๋๋ค.
- SQLAlchemy: ํ์ด์ฌ์ ์ํ ์ต๊ณ ์ SQL ํดํท ๋ฐ ๊ฐ์ฒด ๊ด๊ณํ ๋งคํผ(ORM)์ ๋๋ค. SQLite๋ถํฐ BigQuery ๋๋ Redshift์ ๊ฐ์ ์ํฐํ๋ผ์ด์ฆ๊ธ ์จ์ดํ์ฐ์ค์ ์ด๋ฅด๊ธฐ๊น์ง ๊ฑฐ์ ๋ชจ๋ SQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐํ๊ธฐ ์ํ ์ผ๊ด๋๊ณ ๊ณ ์์ค API๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ์ํฌํ๋ก์ฐ ์ค์ผ์คํธ๋ ์ดํฐ(Airflow, Prefect, Dagster): ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค๋ ๋จ์ผ ์คํฌ๋ฆฝํธ๋ก ๊ตฌ์ถ๋์ง ์์ต๋๋ค. ์ผ๋ จ์ ์ข ์ ์์ (A์์ ์ถ์ถ, B ๋ณํ, C ๋ก๋, D ํ์ธ)์ ๋๋ค. ์ค์ผ์คํธ๋ ์ดํฐ๋ ์ด๋ฌํ ์ํฌํ๋ก๋ฅผ ๋ฐฉํฅ์ฑ ๋น์ํ ๊ทธ๋ํ(DAG)๋ก ์ ์ํ๊ณ , ์์ฝํ๊ณ , ๋ชจ๋ํฐ๋งํ๊ณ , ๊ฐ๋ ฅํ๊ฒ ์ฌ์๋ํ ์ ์๋๋ก ํฉ๋๋ค.
๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ์ฒ๋ฆฌ
- ํด๋ผ์ฐ๋ DWH ์ปค๋ฅํฐ:
snowflake-connector-python,google-cloud-bigquery,psycopg2(Redshift ๋ฐ PostgreSQL์ฉ)์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฃผ์ ํด๋ผ์ฐ๋ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค์ ์ํํ๊ฒ ์ํธ ์์ฉํ ์ ์์ต๋๋ค. - PyArrow: ์ดํ ๋ฐ์ดํฐ ํ์ ์์ ์ ์ํ ์ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ํ์คํ๋ ์ธ๋ฉ๋ชจ๋ฆฌ ํ์์ ์ ๊ณตํ๊ณ ์์คํ ๊ฐ์ ๊ณ ์ ๋ฐ์ดํฐ ์ ์ก์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. Parquet์ ๊ฐ์ ํ์๊ณผ์ ํจ์จ์ ์ธ ์ํธ ์์ฉ์ ์ง์ํ๋ ์์ง์ ๋๋ค.
- ์ต์ ๋ ์ดํฌํ์ฐ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ: ๊ณ ๊ธ ์ค์ ์ ์ํด
deltalake,py-iceberg์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฐ - Spark ์ฌ์ฉ์์ฉ - ์ด๋ฌํ ํ์์ ๋ํ PySpark์ ๋ค์ดํฐ๋ธ ์ง์์ ํตํด ํ์ด์ฌ์ ์ ๋ขฐํ ์ ์๋ ํธ๋์ญ์ ๋ฐ์ดํฐ ๋ ์ดํฌ๋ฅผ ๊ตฌ์ถํ ์ ์์ผ๋ฉฐ, ์ด๋ ์จ์ดํ์ฐ์ค์ ๊ธฐ๋ฐ ์ญํ ์ ํฉ๋๋ค.
ํํธ 3: ํ์ด์ฌ์ผ๋ก OLAP ์์คํ ์ค๊ณ
์ด์ ์ด๋ก ์์ ์ค์ฒ์ผ๋ก ๋์ด๊ฐ๊ฒ ์ต๋๋ค. ๋ถ์ ์์คํ ์ ์ค๊ณํ๊ธฐ ์ํ ๋จ๊ณ๋ณ ๊ฐ์ด๋์ ๋๋ค.
1๋จ๊ณ: ๋ถ์์ ์ํ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง
๋ชจ๋ ์ข์ OLAP ์์คํ ์ ๊ธฐ์ด๋ ๋ฐ์ดํฐ ๋ชจ๋ธ์ ๋๋ค. ๋ชฉํ๋ ๋น ๋ฅด๊ณ ์ง๊ด์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ์ํด ๋ฐ์ดํฐ๋ฅผ ๊ตฌ์กฐํํ๋ ๊ฒ์ ๋๋ค. ๊ฐ์ฅ ์ผ๋ฐ์ ์ด๊ณ ํจ๊ณผ์ ์ธ ๋ชจ๋ธ์ ์คํ ์คํค๋ง์ ๋ณํ์ธ ์ค๋ ธ์ฐํ๋ ์ดํฌ ์คํค๋ง์ ๋๋ค.
์คํ ์คํค๋ง vs. ์ค๋ ธ์ฐํ๋ ์ดํฌ ์คํค๋ง
์คํ ์คํค๋ง๋ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๊ตฌ์กฐ์ ๋๋ค. ๋ค์์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
- ์ค์ ํฉํธ ํ ์ด๋ธ: ์ธก์ ๊ฐ(๋ถ์ํ๋ ค๋ ์ซ์)๊ณผ ์ฐจ์ ํ ์ด๋ธ์ ๋ํ ์ธ๋ ํค๋ฅผ ํฌํจํฉ๋๋ค.
- ์ฌ๋ฌ ์ฐจ์ ํ ์ด๋ธ: ๊ฐ ์ฐจ์ ํ ์ด๋ธ์ ๋จ์ผ ํค๋ก ํฉํธ ํ ์ด๋ธ์ ์กฐ์ธ๋๊ณ ์ค๋ช ์์ฑ์ ํฌํจํฉ๋๋ค. ์ด๋ฌํ ํ ์ด๋ธ์ ๋จ์์ฑ๊ณผ ์๋๋ฅผ ์ํด ๊ณ ๋๋ก ๋น์ ๊ทํ๋ฉ๋๋ค.
์: `DateKey`, `ProductKey`, `StoreKey`, `QuantitySold`, `TotalRevenue` ์ด์ด ์๋ `FactSales` ํ ์ด๋ธ. `DimDate`, `DimProduct`, `DimStore` ํ ์ด๋ธ๋ก ๋๋ฌ์ธ์ผ ๊ฒ์ ๋๋ค.
์ค๋ ธ์ฐํ๋ ์ดํฌ ์คํค๋ง๋ ์ฐจ์ ํ ์ด๋ธ์ด ์ฌ๋ฌ ๊ด๋ จ ํ ์ด๋ธ๋ก ์ ๊ทํ๋ ์คํ ์คํค๋ง์ ํ์ฅ์ ๋๋ค. ์๋ฅผ ๋ค์ด `DimProduct` ํ ์ด๋ธ์ `DimProduct`, `DimBrand`, `DimCategory` ํ ์ด๋ธ๋ก ๋ถํ ๋ ์ ์์ต๋๋ค.
๊ถ์ฅ ์ฌํญ: ์คํ ์คํค๋ง๋ก ์์ํ์ญ์์ค. ์ฟผ๋ฆฌ๊ฐ ๋ ๊ฐ๋จํ๊ณ (์กฐ์ธ ์๊ฐ ์ ์) ์ต์ ์ดํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋๊ณ ๋น์ ๊ทํ๋ ํ ์ด๋ธ์ ์ฒ๋ฆฌํ๋ ๋ฐ ๋งค์ฐ ํจ์จ์ ์ด๋ฏ๋ก ์ค๋ ธ์ฐํ๋ ์ดํฌ ์คํค๋ง์ ์ ์ฅ์ ์ด์ ์ ์ถ๊ฐ ์กฐ์ธ์ผ๋ก ์ธํ ์ฑ๋ฅ ๋น์ฉ์ ๋นํด ์ข ์ข ๋ฌด์ํ ์ ์์ต๋๋ค.
2๋จ๊ณ: ํ์ด์ฌ์์ ETL/ELT ํ์ดํ๋ผ์ธ ๊ตฌ์ถ
ETL ํ๋ก์ธ์ค๋ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค์ ๋ฐ์ดํฐ๋ฅผ ๊ณต๊ธํ๋ ๋ฐฑ๋ณธ์ ๋๋ค. ์์ค ์์คํ ์์ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ๊ณ , ๊นจ๋ํ๊ณ ์ผ๊ด๋ ํ์์ผ๋ก ๋ณํํ๊ณ , ๋ถ์ ๋ชจ๋ธ์ ๋ก๋ํ๋ ๊ณผ์ ์ด ํฌํจ๋ฉ๋๋ค.
Pandas๋ฅผ ์ฌ์ฉํ๋ ๊ฐ๋จํ ํ์ด์ฌ ์คํฌ๋ฆฝํธ๋ก ์ค๋ช ํด ๋ณด๊ฒ ์ต๋๋ค. ์์ ์ฃผ๋ฌธ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋ ์์ค CSV ํ์ผ์ด ์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
# ํ์ด์ฌ ๋ฐ Pandas๋ฅผ ์ฌ์ฉํ ๋จ์ํ๋ ETL ์์
import pandas as pd
# --- ์ถ์ถ ---
print("์์ ์ฃผ๋ฌธ ๋ฐ์ดํฐ ์ถ์ถ ์ค...")
source_df = pd.read_csv('raw_orders.csv')
# --- ๋ณํ ---
print("๋ฐ์ดํฐ ๋ณํ ์ค...")
# 1. ๋ฐ์ดํฐ ์ ๋ฆฌ
source_df['order_date'] = pd.to_datetime(source_df['order_date'])
source_df['product_price'] = pd.to_numeric(source_df['product_price'], errors='coerce')
source_df.dropna(inplace=True)
# 2. ๋ฐ์ดํฐ ๋ณด๊ฐ - ๋ณ๋์ ๋ ์ง ์ฐจ์ ์์ฑ
dim_date = pd.DataFrame({
'DateKey': source_df['order_date'].dt.strftime('%Y%m%d').astype(int),
'Date': source_df['order_date'].dt.date,
'Year': source_df['order_date'].dt.year,
'Quarter': source_df['order_date'].dt.quarter,
'Month': source_df['order_date'].dt.month,
'DayOfWeek': source_df['order_date'].dt.day_name()
}).drop_duplicates().reset_index(drop=True)
# 3. ์ ํ ์ฐจ์ ์์ฑ
dim_product = source_df[['product_id', 'product_name', 'category']].copy()
dim_product.rename(columns={'product_id': 'ProductKey'}, inplace=True)
dim_product.drop_duplicates(inplace=True).reset_index(drop=True)
# 4. ํฉํธ ํ
์ด๋ธ ์์ฑ
fact_sales = source_df.merge(dim_date, left_on=source_df['order_date'].dt.date, right_on='Date')\
.merge(dim_product, left_on='product_id', right_on='ProductKey')
fact_sales = fact_sales[['DateKey', 'ProductKey', 'order_id', 'quantity', 'product_price']]
fact_sales['TotalRevenue'] = fact_sales['quantity'] * fact_sales['product_price']
fact_sales.rename(columns={'order_id': 'OrderCount'}, inplace=True)
# ์ํ๋ ์
๋์ ๋ง๊ฒ ์ง๊ณ
fact_sales = fact_sales.groupby(['DateKey', 'ProductKey']).agg(
TotalRevenue=('TotalRevenue', 'sum'),
TotalQuantity=('quantity', 'sum')
).reset_index()
# --- ๋ก๋ ---
print("๋ฐ์ดํฐ๋ฅผ ๋์ ์ ์ฅ์์ ๋ก๋ ์ค...")
# ์ด ์์ ์์๋ ๋งค์ฐ ํจ์จ์ ์ธ ์ด ํ์์ธ Parquet ํ์ผ์ ์ ์ฅํฉ๋๋ค.
'warehouse/dim_date.parquet')
'warehouse/dim_product.parquet')
'warehouse/fact_sales.parquet')
print("ETL ํ๋ก์ธ์ค ์๋ฃ!")
์ด ๊ฐ๋จํ ์คํฌ๋ฆฝํธ๋ ํต์ฌ ๋ ผ๋ฆฌ๋ฅผ ๋ณด์ฌ์ค๋๋ค. ์ค์ ์๋๋ฆฌ์ค์์๋ ์ด ๋ ผ๋ฆฌ๋ฅผ ํจ์๋ก ๋ํํ๊ณ Airflow์ ๊ฐ์ ์ค์ผ์คํธ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์คํ์ ๊ด๋ฆฌํฉ๋๋ค.
3๋จ๊ณ: OLAP ์์ง ์ ํ ๋ฐ ๊ตฌํ
๋ฐ์ดํฐ๊ฐ ๋ชจ๋ธ๋ง๋๊ณ ๋ก๋๋๋ฉด OLAP ์์ ์ ์ํํ ์์ง์ด ํ์ํฉ๋๋ค. ํ์ด์ฌ ์ธ๊ณ์์๋ ์ฃผ๋ก ROLAP ๋ฐฉ์์ ๋ฐ๋ฅด๋ ๋ช ๊ฐ์ง ๊ฐ๋ ฅํ ์ต์ ์ด ์์ต๋๋ค.
์ต์ A: ๊ฒฝ๋ ๊ฐ๋ ฅํจ - DuckDB
DuckDB๋ ํ์ด์ฌ์์ ์ฌ์ฉํ๊ธฐ ๋งค์ฐ ๋น ๋ฅด๊ณ ์ฌ์ด ์ธํ๋ก์ธ์ค ๋ถ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋๋ค. Pandas DataFrame ๋๋ Parquet ํ์ผ์ SQL์ ์ฌ์ฉํ์ฌ ์ง์ ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค. ์๊ท๋ชจ์์ ์ค๊ท๋ชจ OLAP ์์คํ , ํ๋กํ ํ์ ๋ฐ ๋ก์ปฌ ๊ฐ๋ฐ์ ์๋ฒฝํ ์ ํ์ ๋๋ค.
๊ณ ์ฑ๋ฅ ROLAP ์์ง ์ญํ ์ ํฉ๋๋ค. ํ์ค SQL์ ์์ฑํ๋ฉด DuckDB๊ฐ ๋ฐ์ดํฐ ํ์ผ์ ํตํด ๊ทน๋๋ก ๋น ๋ฅธ ์๋๋ก ์คํํฉ๋๋ค.
import duckdb
# ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ ํ์ผ์ ์ฐ๊ฒฐ
con = duckdb.connect(database=':memory:', read_only=False)
# ์ด์ ์ ์์ฑํ Parquet ํ์ผ์ ์ง์ ์ฟผ๋ฆฌํฉ๋๋ค.
# DuckDB๋ ์คํค๋ง๋ฅผ ์๋์ผ๋ก ์ดํดํฉ๋๋ค.
result = con.execute("""
SELECT
p.category,
d.Year,
SUM(f.TotalRevenue) AS AnnualRevenue
FROM 'warehouse/fact_sales.parquet' AS f
JOIN 'warehouse/dim_product.parquet' AS p ON f.ProductKey = p.ProductKey
JOIN 'warehouse/dim_date.parquet' AS d ON f.DateKey = d.DateKey
WHERE p.category = 'Electronics'
GROUP BY p.category, d.Year
ORDER BY d.Year;
""").fetchdf() # fetchdf()๋ Pandas DataFrame์ ๋ฐํํฉ๋๋ค.
print(result)
์ต์ B: ํด๋ผ์ฐ๋ ๊ท๋ชจ์ ๊ฑฐ์ธ - Snowflake, BigQuery, Redshift
๋๊ท๋ชจ ์ํฐํ๋ผ์ด์ฆ ์์คํ ์ ๊ฒฝ์ฐ ํด๋ผ์ฐ๋ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ค๊ฐ ํ์ค ์ ํ์ ๋๋ค. ํ์ด์ฌ์ ์ด๋ฌํ ํ๋ซํผ๊ณผ ์๋ฒฝํ๊ฒ ํตํฉ๋ฉ๋๋ค. ETL ํ๋ก์ธ์ค๋ ๋ฐ์ดํฐ๋ฅผ ํด๋ผ์ฐ๋ DWH๋ก ๋ก๋ํ๊ณ , ํ์ด์ฌ ์ ํ๋ฆฌ์ผ์ด์ (์: BI ๋์๋ณด๋ ๋๋ Jupyter ๋ ธํธ๋ถ)์ ์ด๋ฅผ ์ฟผ๋ฆฌํฉ๋๋ค.
DuckDB์ ๋์ผํ์ง๋ง ์ฐ๊ฒฐ ๋ฐ ๊ท๋ชจ๊ฐ ๋ค๋ฆ ๋๋ค.
import snowflake.connector
# Snowflake์ ์ฐ๊ฒฐํ๊ณ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ์์
conn = snowflake.connector.connect(
user='your_user',
password='your_password',
account='your_account_identifier'
)
cursor = conn.cursor()
try:
cursor.execute("USE WAREHOUSE MY_WH;")
cursor.execute("USE DATABASE MY_DB;")
cursor.execute("""
SELECT category, YEAR(date), SUM(total_revenue)
FROM fact_sales
JOIN dim_product ON ...
JOIN dim_date ON ...
GROUP BY 1, 2;
""")
# ํ์ํ ๋๋ก ๊ฒฐ๊ณผ ๊ฐ์ ธ์ค๊ธฐ
for row in cursor:
print(row)
finally:
cursor.close()
conn.close()
์ต์ C: ์ค์๊ฐ ์ ๋ฌธ๊ฐ - Apache Druid ๋๋ ClickHouse
๋๊ท๋ชจ ์คํธ๋ฆฌ๋ฐ ๋ฐ์ดํฐ์ (์ค์๊ฐ ์ฌ์ฉ์ ๋ถ์ ๋ฑ)์ ๋ํ ์ด๋น ์ฟผ๋ฆฌ ์ง์ฐ ์๊ฐ์ด ํ์ํ ์ฌ์ฉ ์ฌ๋ก์ ๊ฒฝ์ฐ Druid ๋๋ ClickHouse์ ๊ฐ์ ์ ๋ฌธ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ๋ฅญํ ์ ํ์ ๋๋ค. OLAP ์ํฌ๋ก๋์ฉ์ผ๋ก ์ค๊ณ๋ ์ดํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋๋ค. ํ์ด์ฌ์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์คํธ๋ฆฌ๋ฐํ๊ณ ํด๋น ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋๋ HTTP API๋ฅผ ํตํด ์ฟผ๋ฆฌํฉ๋๋ค.
ํํธ 4: ์ค์ฉ์ ์ธ ์์ - ๋ฏธ๋ OLAP ์์คํ ๊ตฌ์ถ
์ด๋ฌํ ๊ฐ๋ ์ ๊ฒฐํฉํ์ฌ ๋ฏธ๋ ํ๋ก์ ํธ๋ฅผ ์ํํด ๋ณด๊ฒ ์ต๋๋ค. ๋ํํ ํ๋งค ๋์๋ณด๋์ ๋๋ค. ์ด๋ ์์ ํ๊ณ ๋จ์ํ๋ ํ์ด์ฌ ๊ธฐ๋ฐ OLAP ์์คํ ์ ๋ณด์ฌ์ค๋๋ค.
๋น์ฌ์ ์คํ:
- ETL: ํ์ด์ฌ ๋ฐ Pandas
- ๋ฐ์ดํฐ ์ ์ฅ: Parquet ํ์ผ
- OLAP ์์ง: DuckDB
- ๋์๋ณด๋: Streamlit (๋ฐ์ดํฐ ๊ณผํ์ ์ํ ์๋ฆ๋ต๊ณ ๋ํํ ์น ์ฑ์ ๋ง๋๋ ์คํ์์ค ํ์ด์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
๋จผ์ 3๋ถ์ ETL ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ์ฌ `warehouse/` ๋๋ ํ ๋ฆฌ์ Parquet ํ์ผ์ ์์ฑํฉ๋๋ค.
๋ค์์ผ๋ก ๋์๋ณด๋ ์ ํ๋ฆฌ์ผ์ด์ ํ์ผ์ธ `app.py`๋ฅผ ๋ง๋ญ๋๋ค.
# app.py - ๊ฐ๋จํ ๋ํํ ํ๋งค ๋์๋ณด๋
import streamlit as st
import duckdb
import pandas as pd
import plotly.express as px
# --- ํ์ด์ง ๊ตฌ์ฑ ---
st.set_page_config(layout="wide", page_title="์ ์ฒด ํ๋งค ๋์๋ณด๋")
st.title("๋ํํ ํ๋งค OLAP ๋์๋ณด๋")
# --- DuckDB ์ฐ๊ฒฐ ---
# ์ด๊ฒ์ Parquet ํ์ผ์ ์ง์ ์ฟผ๋ฆฌํฉ๋๋ค.
con = duckdb.connect(database=':memory:', read_only=True)
# --- ํํฐ๋ฅผ ์ํ ์ฐจ์ ๋ฐ์ดํฐ ๋ก๋ ---
@st.cache_data
def load_dimensions():
products = con.execute("SELECT DISTINCT category FROM 'warehouse/dim_product.parquet'").fetchdf()
years = con.execute("SELECT DISTINCT Year FROM 'warehouse/dim_date.parquet' ORDER BY Year").fetchdf()
return products['category'].tolist(), years['Year'].tolist()
categories, years = load_dimensions()
# --- ํํฐ๋ฅผ ์ํ ์ฌ์ด๋๋ฐ (์ฌ๋ผ์ด์ฑ ๋ฐ ๋ค์ด์ฑ!) ---
st.sidebar.header("OLAP ํํฐ")
selected_categories = st.sidebar.multiselect(
'์ ํ ์นดํ
๊ณ ๋ฆฌ ์ ํ',
options=categories,
default=categories
)
selected_year = st.sidebar.selectbox(
'์ฐ๋ ์ ํ',
options=years,
index=len(years)-1 # ์ต์ ์ฐ๋๋ก ๊ธฐ๋ณธ ์ค์
)
# --- OLAP ์ฟผ๋ฆฌ ๋์ ๋น๋ ---
if not selected_categories:
st.warning("์ต์ํ ํ๋์ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ์ ํํ์ญ์์ค.")
st.stop()
query = f"""
SELECT
d.Month,
d.MonthName, -- DimDate์ MonthName์ด ์๋ค๊ณ ๊ฐ์
p.category,
SUM(f.TotalRevenue) AS Revenue
FROM 'warehouse/fact_sales.parquet' AS f
JOIN 'warehouse/dim_product.parquet' AS p ON f.ProductKey = p.ProductKey
JOIN 'warehouse/dim_date.parquet' AS d ON f.DateKey = d.DateKey
WHERE d.Year = {selected_year}
AND p.category IN ({str(selected_categories)[1:-1]})
GROUP BY d.Month, d.MonthName, p.category
ORDER BY d.Month;
"""
# --- ์ฟผ๋ฆฌ ์คํ ๋ฐ ๊ฒฐ๊ณผ ํ์ ---
@st.cache_data
def run_query(_query):
return con.execute(_query).fetchdf()
results_df = run_query(query)
if results_df.empty:
st.info(f"{selected_year}๋
์ ์ ํํ ํํฐ์ ๋ํ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.")
else:
# --- ๋ฉ์ธ ๋์๋ณด๋ ์๊ฐํ ---
col1, col2 = st.columns(2)
with col1:
st.subheader(f"{selected_year}๋
์๋ณ ์์ต")
fig = px.line(
results_df,
x='MonthName',
y='Revenue',
color='category',
title='์นดํ
๊ณ ๋ฆฌ๋ณ ์๋ณ ์์ต'
)
st.plotly_chart(fig, use_container_width=True)
with col2:
st.subheader("์นดํ
๊ณ ๋ฆฌ๋ณ ์์ต")
category_summary = results_df.groupby('category')['Revenue'].sum().reset_index()
fig_pie = px.pie(
category_summary,
names='category',
values='Revenue',
title='์นดํ
๊ณ ๋ฆฌ๋ณ ์ด ์์ต ์ ์ ์จ'
)
st.plotly_chart(fig_pie, use_container_width=True)
st.subheader("์์ธ ๋ฐ์ดํฐ")
st.dataframe(results_df)
์ด๊ฒ์ ์คํํ๋ ค๋ฉด ์ฝ๋๋ฅผ `app.py`๋ก ์ ์ฅํ๊ณ ํฐ๋ฏธ๋์์ `streamlit run app.py`๋ฅผ ์คํํฉ๋๋ค. ๋ํํ ๋์๋ณด๋๊ฐ ์๋ ์น ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฆฝ๋๋ค. ์ฌ์ด๋๋ฐ์ ํํฐ๋ฅผ ํตํด ์ฌ์ฉ์๋ OLAP '์ฌ๋ผ์ด์ฑ' ๋ฐ '๋ค์ด์ฑ' ์์ ์ ์ํํ ์ ์์ผ๋ฉฐ, ๋์๋ณด๋๋ DuckDB๋ฅผ ๋ค์ ์ฟผ๋ฆฌํ์ฌ ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค.
ํํธ 5: ๊ณ ๊ธ ์ฃผ์ ๋ฐ ๋ชจ๋ฒ ์ฌ๋ก
๋ฏธ๋ ํ๋ก์ ํธ์์ ํ๋ก๋์ ์์คํ ์ผ๋ก ์ด๋ํจ์ ๋ฐ๋ผ ์ด๋ฌํ ๊ณ ๊ธ ์ฃผ์ ๋ฅผ ๊ณ ๋ คํ์ญ์์ค.
ํ์ฅ์ฑ ๋ฐ ์ฑ๋ฅ
- ๋๊ท๋ชจ ETL์๋ Dask ์ฌ์ฉ: ์์ค ๋ฐ์ดํฐ๊ฐ ๋จธ์ RAM์ ์ด๊ณผํ๋ ๊ฒฝ์ฐ ETL ์คํฌ๋ฆฝํธ์์ Pandas๋ฅผ Dask๋ก ๋ฐ๊พธ์ญ์์ค. API๋ ๋งค์ฐ ์ ์ฌํ์ง๋ง Dask๋ ์คํ์ฝ์ด ๋ฐ ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
- ์ดํ ์ ์ฅ์๊ฐ ํต์ฌ์ ๋๋ค: ์จ์ดํ์ฐ์ค ๋ฐ์ดํฐ๋ฅผ ํญ์ Apache Parquet ๋๋ ORC์ ๊ฐ์ ์ดํ ํ์์ผ๋ก ์ ์ฅํ์ญ์์ค. ์ด๋ ๊ฒ ํ๋ฉด ์ผ๋ฐ์ ์ผ๋ก ์์ด๋ ํ ์ด๋ธ์์ ๋ช ๊ฐ์ ์ด๋ง ์ฝ์ผ๋ฉด ๋๋ ๋ถ์ ์ฟผ๋ฆฌ๊ฐ ํจ์ฌ ๋นจ๋ผ์ง๋๋ค.
- ํํฐ์ ๋: ๋ฐ์ดํฐ ๋ ์ดํฌ(S3 ๋๋ ๋ก์ปฌ ํ์ผ ์์คํ ๋ฑ)์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ ๋ ์ง์ ๊ฐ์ ์์ฃผ ํํฐ๋ง๋๋ ์ฐจ์์ ๊ธฐ์ค์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ํด๋๋ก ๋ถํ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด: `warehouse/fact_sales/year=2023/month=12/`. ์ด๋ ๊ฒ ํ๋ฉด ์ฟผ๋ฆฌ ์์ง์ด ๊ด๋ จ ์๋ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ฒ์ ๊ฑด๋๋ธ ์ ์์ผ๋ฉฐ, ์ด๋ 'ํํฐ์ ๊ฐ์ง์น๊ธฐ'๋ผ๊ณ ํฉ๋๋ค.
์๋ฏธ ๊ณ์ธต
์์คํ ์ด ์ฑ์ฅํจ์ ๋ฐ๋ผ ์ฌ๋ฌ ์ฟผ๋ฆฌ์ ๋์๋ณด๋์์ 'ํ์ฑ ์ฌ์ฉ์' ๋๋ '์ด ๋ง์ง'์ ์ ์์ ๊ฐ์ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ฐ๋ณต๋๋ ๊ฒ์ ๋ฐ๊ฒฌํ๊ฒ ๋ฉ๋๋ค. ์๋ฏธ ๊ณ์ธต์ ๋น์ฆ๋์ค ์ธก์ ๊ฐ ๋ฐ ์ฐจ์์ ๋ํ ์ค์ ์ง์ค์์ ์ผ๊ด๋ ์ ์๋ฅผ ์ ๊ณตํ์ฌ ์ด๋ฅผ ํด๊ฒฐํฉ๋๋ค. dbt(Data Build Tool)์ ๊ฐ์ ๋๊ตฌ๋ ์ด๋ฅผ ์ํด ํ์ํฉ๋๋ค. dbt ์์ฒด๋ ํ์ด์ฌ ๋๊ตฌ๊ฐ ์๋์ง๋ง ํ์ด์ฌ ์ค์ผ์คํธ๋ ์ด์ ์ํฌํ๋ก์ ์๋ฒฝํ๊ฒ ํตํฉ๋ฉ๋๋ค. dbt๋ฅผ ์ฌ์ฉํ์ฌ ์คํ ์คํค๋ง๋ฅผ ๋ชจ๋ธ๋งํ๊ณ ์ธก์ ๊ฐ์ ์ ์ํ ๋ค์, ํ์ด์ฌ์ ์ฌ์ฉํ์ฌ dbt ์คํ์ ์ค์ผ์คํธ๋ ์ด์ ํ๊ณ ๊ฒฐ๊ณผ ํ ์ด๋ธ์ ๋ถ์ํ ์ ์์ต๋๋ค.
๋ฐ์ดํฐ ๊ฑฐ๋ฒ๋์ค ๋ฐ ํ์ง
์จ์ดํ์ฐ์ค๋ ๋ด๋ถ ๋ฐ์ดํฐ๋งํผ๋ง ์ ์ฉํฉ๋๋ค. ํ์ด์ฌ ETL ํ์ดํ๋ผ์ธ์ ๋ฐ์ดํฐ ํ์ง ๊ฒ์ฌ๋ฅผ ์ง์ ํตํฉํ์ญ์์ค. Great Expectations์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ์ ๋ํ '๊ธฐ๋์น'(์: `customer_id`๋ ์ ๋ null์ด ์๋์ด์ผ ํจ, `revenue`๋ 0์์ 1,000,000 ์ฌ์ด์ฌ์ผ ํจ)๋ฅผ ์ ์ํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ETL ์์ ์ด ๋ค์ด์ค๋ ๋ฐ์ดํฐ๊ฐ ์ด๋ฌํ ๊ณ์ฝ์ ์๋ฐํ๋ฉด ์คํจํ๊ฑฐ๋ ๊ฒฝ๊ณ ํ ์ ์์ด ์จ์ดํ์ฐ์ค๋ฅผ ์์์ํค๋ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
๊ฒฐ๋ก : ์ฝ๋ ์ฐ์ ์ ๊ทผ ๋ฐฉ์์ ํ
ํ์ด์ฌ์ ๋ฐ์ดํฐ ์จ์ดํ์ฐ์ง ๋ฐ ๋น์ฆ๋์ค ์ธํ ๋ฆฌ์ ์ค ํ๊ฒฝ์ ๊ทผ๋ณธ์ ์ผ๋ก ๋ณํ์์ผฐ์ต๋๋ค. ์ฒ์๋ถํฐ ๋ณต์กํ ๋ถ์ ์์คํ ์ ๊ตฌ์ถํ๊ธฐ ์ํ ์ ์ฐํ๊ณ ๊ฐ๋ ฅํ๋ฉฐ ๊ณต๊ธ์ ์ฒด์ ๊ตฌ์ ๋ฐ์ง ์๋ ํดํท์ ์ ๊ณตํฉ๋๋ค. Pandas, Dask, SQLAlchemy ๋ฐ DuckDB์ ๊ฐ์ ์ ๊ณ ์ต๊ณ ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฒฐํฉํ๋ฉด ํ์ฅ ๊ฐ๋ฅํ๊ณ ์ ์ง ๊ด๋ฆฌ ๊ฐ๋ฅํ ์์ ํ OLAP ์์คํ ์ ๋ง๋ค ์ ์์ต๋๋ค.
์ฌ์ ์ ์คํ ์คํค๋ง์ ๊ฐ์ ๊ฒฌ๊ณ ํ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง ์์น์ ๋ํ ํ์คํ ์ดํด์์ ์์๋ฉ๋๋ค. ๊ฑฐ๊ธฐ์์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ์ํํ๊ธฐ ์ํ ๊ฐ๋ ฅํ ETL ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํ๊ณ , ๊ท๋ชจ์ ๋ง๋ ์ฟผ๋ฆฌ ์์ง์ ์ ํํ๊ณ , ๋ํํ ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์๋ ์์ต๋๋ค. ์ข ์ข '๋ชจ๋ ๋ฐ์ดํฐ ์คํ'์ ํต์ฌ ์์น์ธ ์ด ์ฝ๋ ์ฐ์ ์ ๊ทผ ๋ฐฉ์์ ๋ถ์์ ํ์ ๊ฐ๋ฐ์์ ๋ฐ์ดํฐ ํ์ ์์ ์ง์ ๋ฃ์ด ์กฐ์ง์ ์๊ตฌ์ ์๋ฒฝํ๊ฒ ๋ง์ถคํ๋ ์์คํ ์ ๊ตฌ์ถํ ์ ์๋๋ก ํฉ๋๋ค.